上一篇博客介绍了事件传递的基本流程,为了做到知其然并知其所以然,本文从源码的角度来分析一下Android事件传的流程。
再分析代码之前,我们先用 Android Studio 来看一下事件传过程中的调用栈。
我们在前文中的默认情况下(不拦截和处理事件)在 ViewC 的 onTouchEvent
中添加断点。
调用栈如图所示:
看了上篇博客我们也许会有疑问,事件的传递流程源头是不是从 Activity
开的,看了上图就有答案了,源头是从系统的事件分发系统开始,首先传递给 DecorView
开始的。
下面我们源码的分析的思路就按照上面的事件传递的流程顺序来进行。
调用流程先用下面的流程来表示一下:
1 |
事件起源
DecorView
接收事件之前的流程我们这不做详细介绍,只介绍 View
部分的事件传递。在 Activit
接收到事件之前的传递流程是:
1 | ├── View.dispatchPointerEvent |
先来看一下 View.dispatchPointerEvent
,这里其实执行的是 DecorView
对象的 dispatchPointerEvent
:
1 | public final boolean dispatchPointerEvent(MotionEvent event) { |
View.dispatchPointerEvent
方法是final类型的,是不可以被 Override
的。
如果是触摸事件,则调用 DecorView
的 dispatchTouchEvent
方法。
1 | @Override |
Activity 的事件分发
上一篇博客中介绍事件传递的开是 Activit,Activity 首先进行事的分发,那么下面先来看一下 Activity.dispatchTouchEvent(MotionEvent ev)
方法。在此之的流程这里先不涉及介绍。
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
这个方法主是调用了 getWindow().superDispatchTouchEvent(ev)
方法,如果该方法返回false,那么再调用 Activity.onTouchEvent(ev)
来处理事件。
Activity 中的 mWindow
在 Activity.attach()
中被实例化,是一个 PhoneWindow
对象。
1 | mWindow = new PhoneWindow(this); |
那么我们就再来看一下 PhoneWindow.superDispatchTouchEvent
方法。
1 | @Override |
这里调用了 DecorView
的 superDispatchTouchEvent
方法。
1 | public boolean superDispatchTouchEvent(MotionEvent event) { |
DecorView.superDispatchTouchEvent
直接调了它的 dispatchTouchEvent
方法,我们知,DecorView
是继承了 FrameLayout
的,那么其实就是调了 ViewGroup
的 dispatchTouchEvent
方法。
ViewGroup 的事件分发
来看一下 ViewGroup.dispatchTouchEvent
方法,这个方法比较长,我们只选择事件传递的关键代码进行分析。
先来看下面一段代码,这段代码主要做一些事件派发前的准备工作:
1 | @Override |
上面的一段代码主要是做了一些事件派发前的准备工作,判断是否对当前事件进行拦截。
首先第一个条件是当前事件是否是 ACTION_DOWN
或者 mFirstTouchTarget != null
,判断当前是否是 ACTION_DOWN
事件我们容易理解,上一篇博客的例子我们也知道,ACTION_DOWN
事件是一个手势事件系列的开始,是会调用 onInterceptTouchEvent
的。那么 mFirstTouchTarget != null
是什么呢?从 dispatchTouchEvent
后面的代码我们就可以看出来,当事件被 ViewGroup
的子元素处理时,mFirstTouchTarget
就会指向这个子元素。也就是说,如果当前 ViewGroup
拦截该事件时,mFirstTouchTarget != null
这个条件是不成立的,那么当后面的 ACTION_MOVE
和 ACTION_UP
事件到来时,将不会执行这段代码,那么 onInterceptTouchEvent
方法就不会被调用。如果有子元素处理事件,那么后面的 ACTION_MOVE
和 ACTION_UP
事件到来时,将会执行这段代码,那么 ViewGroup
就会有机会拦截事件的传递。
这里还有另外一个判断条件,就是是否设置了 FLAG_DISALLOW_INTERCEPT
这个标志位,这个标志位一般是子 View
通过 ViewGroup.requestDisallowInterceptTouchEvent
方法来设置的,一旦设置,ViewGroup
将无法拦截除了 DOWN 事件的其他一切事件。
为什么说可以拦截 DOWN 事件呢?因为 ViewGroup
在事件分发时,如果是 DOWN 事件就重置 FLAG_DISALLOW_INTERCEPT
标志位,这一点从上面的源码中也可以看到,这个操作将会导致子 View 设置的该标志位无效,因此当分发 DOWN 事件时,总是会调用 onInterceptTouchEvent
来询问是否要拦截该事件。
从上面的代码可以得到下面几点结论:
- 当 ViewGroup 决定拦截事件后,那么后续的事件将会默认交给它来处理而不再调用
onInterceptTouchEvent
方法。 FLAG_DISALLOW_INTERCEPT
这个标志位的作用是不让它的父View们中途来拦截事件,当然父View没有拦截DOWN事件,这个标志位无法操控父View对DOWN事件的拦截。onInterceptTouchEvent
不是每次都调用,如果我们想操控所有的点击事件,只能在dispatchTouchEvent
方法中处理。只有这个方法保证每次都会被调用(前提是事件能传递到当前的ViewGroup
)。- 子 View 调用父 View 的
requestDisallowInterceptTouchEvent
一定要注意调用时机,否则是不会生效的。
下面我们接着看当 ViewGroup
不拦截事件时的处理,主要是进行派发目标的查找。
1 | @Override |
从上面源码可以看到,当 ViewGroup
不拦截事件时,事件会向下分发交由它的子 View 处理。这个事件必须是ACTION_DOWN或ACTION_POINTER_DOWN,即新的事件序列或子序列的开始,才会进行派发事件查找。
首先会逆序遍历一遍所有的子元素,判断元素是否能接收点击事件,条件是判断子View是否可以处理、是否在做动画或者点击事件的坐标是否在子View范围之内来作为依据,如果可以接收事件,再通过 getTouchTarget 来寻找是否有 TouchTarget 来处理,如果找到,则意味着之前已经有触摸点落于该child且消费了事件,那么只需要给其添加触摸点ID,然后结束子view遍历;如果没有找到 TouchTarget ,说明对于该child是新的事件,则调用 dispatchTransformedTouchEvent
方法对其进行派发。
若 dispatchTransformedTouchEvent
找到child消费事件,则创建TouchTarget添加至mFirstTouchTarget链表,并标记已经派发过事件。
注意:这里先前存在TouchTarget的情况下不执行dispatchTransformedTouchEvent,是因为需要对当次事件进行事件拆分,对ACTION_POINTER_DOWN类型进行转化,所以留到后面执行派发阶段,再统一处理。
当遍历完子view,若没有找到派发目标,但是mFirstTouchTarget链表不为空,则把最早添加的那个TouchTarget当作查找到的目标。
那么再来看一下这个方法:
1 | private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, |
如果子元素为null则调用父类的 dispatchTouchEvent
,如果子元素不为null,则调用子元素的 dispatchTouchEvent
。
关于对多点触控事件的分发,后面会有专门的文章来介绍。
再来回到 dispatchTouchEvent
方法,如果遍历子元素后事件都没有被处理,这包含两种情况:一是 ViewGroup
没有子元素,二是子元素处理了点击事件,但是子元素 dispatchTouchEvent
返回了 false,这时 ViewGroup
会自己处理事件。这时还是会调用 dispatchTransformedTouchEvent
,只是 child 参数为 null,这是根据上面的的代码,会调用 super.dispatchTouchEvent
,即 View.dispatchTouchEvent
来处理。
1 | @Override |
我们可以看到 ViewGroup
是没有覆盖父类的 onTouchEvent
方法的。也没有发现调用 onTouchEvent
。
对 ViewGroup
的 onTouchEvent
调用,都是通过其父类 View
的 dispatchTouchEvent
来调用的。
流程是什么呢?其实上面也介绍了:ViewGroup.dispatchTouchEvent->ViewGroup.dispatchTransformedTouchEvent->View.dispatchTouchEvent->B.onTouchEvent。
View 的事件分发
先来看一下 View
的 dispatchTouchEvent
方法。
1 | public boolean dispatchTouchEvent(MotionEvent event) { |
View
这里对事件的处理比较简单,这里的处理包含两种类型:一类是由父元素调用子元素的 dispatchTouchEvent
分发而来,第二类是 ViewGroup
没有子元素,调用 super.dispatchTouchEvent
而来。
这里对事件的处理也比较简单,如果没有满足下面的两个条件就会调用 onTouchEvent
来处理事件,即:该 View
是使能状态;设置了 OnTouchListener
且 onTouch
方法返回 true。否则就会调用 onTouchEvent
。
从这里可以看到 OnTouchListener
的优先级是高于 onTouchEvent
的,这样做的好处是方便在外界处理事件。
接下来再看一下 View
的 onTouchEvent
方法。
1 | public boolean onTouchEvent(MotionEvent event) { |
上面一段代码是处理 View
在不可用的情况下对事件的处理。通过阅读代码我们可以得到下面的结论:
- 不可用状态下的
View
在设置可点击(CLICKABLE
、LONG_CLICKABLE
或CONTEXT_CLICKABLE
)的情况下仍然会消费事件。
下面再来看一下 onTouchEvent
对具体事件的处理:
1 | public boolean onTouchEvent(MotionEvent event) { |
1 | public boolean performClick() { |
从上面的代码可以得到如下结论:
- 设置可点击(
CLICKABLE
、LONG_CLICKABLE
或CONTEXT_CLICKABLE
)满足任意一个条件,View
就会消费这个事件,onTouchEvent
就会返回 true,不管是否是 DISABLE 状态。 - 当
ACTION_UP
事件触发时,会调用performClick()
方法。performClick()
会调用OnClickListener
的onClick
方法。
事件处理流程图
下图表示了在事件不被消费的情况下 DOWN
事件的处理流程: